/*
* The MIT License
*
* Copyright (c) 2016, CloudBees, Inc..
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.cloudbees.plugins.credentials;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import com.cloudbees.plugins.credentials.domains.Domain;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials;
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.ExtensionList;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.Action;
import hudson.model.BuildListener;
import hudson.model.Descriptor;
import hudson.model.FreeStyleBuild;
import hudson.model.FreeStyleProject;
import hudson.model.Job;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.StreamBuildListener;
import hudson.model.TaskListener;
import hudson.scm.ChangeLogParser;
import hudson.scm.ChangeLogSet;
import hudson.scm.PollingResult;
import hudson.scm.RepositoryBrowser;
import hudson.scm.SCM;
import hudson.scm.SCMDescriptor;
import hudson.scm.SCMRevisionState;
import hudson.tasks.Builder;
import hudson.triggers.SCMTrigger;
import hudson.triggers.Trigger;
import hudson.util.Secret;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.GregorianCalendar;
import java.util.List;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.TestExtension;
import org.xml.sax.SAXException;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertThat;
public class CredentialsUnavailableExceptionTest {
@Rule
public JenkinsRule r = new JenkinsRule();
private CredentialsStore systemStore;
@Before
public void setUp() throws Exception {
SystemCredentialsProvider.ProviderImpl system = ExtensionList.lookup(CredentialsProvider.class).get(
SystemCredentialsProvider.ProviderImpl.class);
systemStore = system.getStore(r.getInstance());
List<Domain> domainList = new ArrayList<Domain>(systemStore.getDomains());
domainList.remove(Domain.global());
for (Domain d : domainList) {
systemStore.removeDomain(d);
}
List<Credentials> credentialsList = new ArrayList<Credentials>(systemStore.getCredentials(Domain.global()));
for (Credentials c : credentialsList) {
systemStore.removeCredentials(Domain.global(), c);
}
}
@Test
public void buildFailure() throws Exception {
systemStore.addCredentials(Domain.global(), new UsernameUnavailablePasswordImpl("buildFailure", "test", "foo"));
FreeStyleProject project = r.createFreeStyleProject();
project.getBuildersList().add(new PasswordBuildStep("buildFailure"));
FreeStyleBuild build = project.scheduleBuild2(0).get();
this.r.assertBuildStatus(Result.FAILURE, build);
this.r.assertLogContains("username: foo", build);
this.r.assertLogContains("Property 'password' is currently unavailable", build);
this.r.assertLogNotContains("Could not find", build);
this.r.assertLogNotContains("Extracted secret", build);
}
@Test
public void checkoutFailure() throws Exception {
systemStore.addCredentials(Domain.global(), new UsernameUnavailablePasswordImpl("checkoutFailure", "test", "bar"));
FreeStyleProject project = r.createFreeStyleProject();
project.setScm(new PasswordSCM("checkoutFailure"));
FreeStyleBuild build = project.scheduleBuild2(0).get();
this.r.assertBuildStatus(Result.FAILURE, build);
this.r.assertLogContains("user: bar", build);
this.r.assertLogContains("Property 'password' is currently unavailable", build);
this.r.assertLogNotContains("Could not find", build);
this.r.assertLogNotContains("Checking out with password", build);
}
@Test
public void pollingFailure() throws Exception {
UsernameUnavailablePasswordImpl credentials =
new UsernameUnavailablePasswordImpl("pollingFailure", "test", "manchu");
systemStore.addCredentials(Domain.global(),
credentials);
FreeStyleProject project = r.createFreeStyleProject();
project.setQuietPeriod(0);
// ensure we have a build so that polling doesn't trigger a build by accident
r.buildAndAssertSuccess(project);
project.setScm(new PasswordSCM("pollingFailure"));
SCMTrigger trigger = new SCMTrigger("* * * * *");
project.addTrigger(trigger);
trigger.start(project, true);
GregorianCalendar cal = new GregorianCalendar();
cal.add(Calendar.MINUTE, 1);
int number = project.getLastBuild().getNumber();
// now we trigger polling the first time...
Trigger.checkTriggers(cal);
// we should get here without an exception being thrown or else core is handling the runtime exceptions poorly
r.waitUntilNoActivity();
SCMTrigger.SCMAction action = getScmAction(trigger);
assertThat(action.getLog(), allOf(
containsString("Checking remote revision as user: manchu"),
containsString("Property 'password' is currently unavailable")));
assertThat("No new builds", project.getLastBuild().getNumber(), is(number));
cal.add(Calendar.MINUTE, 1);
// now we trigger polling the second time to verify that polling is not stuck
Trigger.checkTriggers(cal);
r.waitUntilNoActivity();
action = getScmAction(trigger);
assertThat(action.getLog(), allOf(
containsString("Checking remote revision as user: manchu"),
containsString("Property 'password' is currently unavailable")));
assertThat("No new builds", project.getLastBuild().getNumber(), is(number));
systemStore.updateCredentials(Domain.global(), credentials, new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "pollingFailure", "working", "manchu", "secret"));
// now we trigger polling the third time... now with working credentials
Trigger.checkTriggers(cal);
r.waitUntilNoActivity();
action = getScmAction(trigger);
assertThat(action.getLog(), allOf(
containsString("Checking remote revision as user: manchu"),
not(containsString("Property 'password' is currently unavailable")),
containsString("Checking remote revision with password: secret")));
assertThat("New build", project.getLastBuild().getNumber(), greaterThan(number));
}
private SCMTrigger.SCMAction getScmAction(SCMTrigger trigger) {
Collection<? extends Action> actions = trigger.getProjectActions();
for (Action a : actions) {
if (a instanceof SCMTrigger.SCMAction) {
return (SCMTrigger.SCMAction)a;
}
}
return null;
}
public static class PasswordSCM extends SCM {
private final String id;
public PasswordSCM(String id) {
this.id = id;
}
@Override
public boolean supportsPolling() {
return true;
}
@Override
public boolean requiresWorkspaceForPolling() {
return false;
}
@Override
public void checkout(@Nonnull Run<?, ?> build, @Nonnull Launcher launcher, @Nonnull FilePath workspace,
@Nonnull TaskListener listener, @javax.annotation.CheckForNull File changelogFile,
@javax.annotation.CheckForNull SCMRevisionState baseline)
throws IOException, InterruptedException {
StandardUsernamePasswordCredentials credentials =
CredentialsProvider.findCredentialById(this.id, StandardUsernamePasswordCredentials.class, build);
if (credentials == null) {
listener.getLogger().printf("Could not find credentials with id '%s'%n", id);
build.setResult(Result.UNSTABLE);
} else {
listener.getLogger().printf("Checking out as user: %s%n", credentials.getUsername());
Secret password = credentials.getPassword();
listener.getLogger().printf("Checking out with password: %s%n", password.getPlainText());
}
}
@Override
public SCMRevisionState calcRevisionsFromBuild(@Nonnull Run<?, ?> build, @Nullable FilePath workspace,
@Nullable Launcher launcher, @Nonnull TaskListener listener)
throws IOException, InterruptedException {
return new SCMRevisionState() {
@Override
public String getIconFileName() {
return "mock.png";
}
@Override
public String getDisplayName() {
return "mock";
}
@Override
public String getUrlName() {
return "mock";
}
};
}
@Override
public PollingResult compareRemoteRevisionWith(@Nonnull Job<?, ?> project, @Nullable Launcher launcher,
@Nullable FilePath workspace, @Nonnull TaskListener listener,
@Nonnull SCMRevisionState baseline)
throws IOException, InterruptedException {
StandardUsernamePasswordCredentials credentials = CredentialsMatchers.firstOrNull(
CredentialsProvider.lookupCredentials(StandardUsernamePasswordCredentials.class, project,
CredentialsProvider.getDefaultAuthenticationOf(project),
Collections.<DomainRequirement>emptyList()), CredentialsMatchers.withId(id));
if (credentials == null) {
throw new IOException(String.format("Could not find credentials with id '%s'", id));
} else {
listener.getLogger().printf("Checking remote revision as user: %s%n", credentials.getUsername());
Secret password = credentials.getPassword();
listener.getLogger()
.printf("Checking remote revision with password: %s%n", password.getPlainText());
}
return PollingResult.SIGNIFICANT;
}
@Override
public ChangeLogParser createChangeLogParser() {
return new ChangeLogParser() {
@Override
public ChangeLogSet<? extends ChangeLogSet.Entry> parse(Run build, RepositoryBrowser<?> browser,
File changelogFile)
throws IOException, SAXException {
return ChangeLogSet.createEmpty(build);
}
};
}
@TestExtension
public static class DescriptorImpl extends SCMDescriptor<PasswordSCM> {
public DescriptorImpl() {
super(RepositoryBrowser.class);
}
@Override
public String getDisplayName() {
return "Password SCM";
}
}
}
public static class PasswordBuildStep extends Builder {
private final String id;
public PasswordBuildStep(String id) {
this.id = id;
}
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener)
throws InterruptedException, IOException {
StandardUsernamePasswordCredentials credentials =
CredentialsProvider.findCredentialById(this.id, StandardUsernamePasswordCredentials.class, build);
if (credentials == null) {
listener.getLogger().printf("Could not find credentials with id '%s'%n", id);
build.setResult(Result.UNSTABLE);
} else {
listener.getLogger().printf("Credentials with id '%s', username: %s%n", id, credentials.getUsername());
Secret password = credentials.getPassword();
listener.getLogger().printf("Extracted secret: %s%n", password.getPlainText());
}
return true;
}
@TestExtension
public static class DescriptorImpl extends Descriptor<Builder> {
@Override
public String getDisplayName() {
return "Password buildstep";
}
}
}
public static class UsernameUnavailablePasswordImpl extends BaseStandardCredentials implements StandardUsernamePasswordCredentials {
private final String username;
public UsernameUnavailablePasswordImpl(@CheckForNull String id, @CheckForNull String description,
String username) {
super(id, description);
this.username = username;
}
public UsernameUnavailablePasswordImpl(@CheckForNull CredentialsScope scope, @CheckForNull String id,
@CheckForNull String description, String username) {
super(scope, id, description);
this.username = username;
}
@NonNull
@Override
public Secret getPassword() {
throw new CredentialsUnavailableException("password");
}
@NonNull
@Override
public String getUsername() {
return username;
}
@TestExtension
public static class DescriptorImpl extends BaseStandardCredentialsDescriptor {
@Override
public String getDisplayName() {
return "Username and unavailable password";
}
}
}
}